Skip to content

feat(share): short spoken codes for live session invites - #135

Merged
kipavy merged 3 commits into
devfrom
feat/session-short-codes
Aug 17, 2026
Merged

feat(share): short spoken codes for live session invites#135
kipavy merged 3 commits into
devfrom
feat/session-short-codes

Conversation

@kipavy

@kipavy kipavy commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Client half of #65: hosts can mint a short code for a live session, and guests can join with one. The server half is already merged (VoltiusApp/server#7) and live in production.

What a host sees

The Link tab keeps the voltius://join deep link exactly as it is — same bytes displayed and copied, same styling, still auto-copied on generate. Underneath it, a second row appears:

  • before minting: Get a code to read aloud
  • after minting: K7M2-P9QX-3B, a copy button, expires in 9:42, and New code
  • on expiry: the row drops the code and offers a fresh one

Placement was chosen from mockups rather than guessed. Two alternatives were rejected: a Link/Code toggle sharing one field (it makes the displayed and copied values mode-dependent — the exact divergence PR #114 had to fix), and replacing the link outright (it would delete the clickable artifact #69 just shipped).

The code is minted on demand, not alongside the link, so its ten minutes start when the host actually needs to say it. InviteCodeField is reused as-is for the code row, so display-equals-clipboard holds by construction rather than by a second implementation.

What a guest sees

A code works anywhere the existing invite shapes already work — the Join by code modal and the omni palette. Detection stays synchronous on shape (10 Crockford symbols, I/L1, O0), so typing in search never hits the API; only activating a join costs a request.

src/services/resolveJoinInput.ts is the single place that turns any of the three shapes into { sessionId, inviteToken }. Both call sites use it, so the branch exists once. parseInviteCode was deliberately left pure and synchronous — a short code cannot be resolved locally, so making it the "third form" inside that function would have forced every caller to become async.

Error mapping is honest about what the server can distinguish: unknown, malformed, expired and revoked codes all answer 404, so they all read "expired or incorrect". 429 gets its own message.

Two changes beyond the code row

inviteToken is now retained on the connection. The server returns it once, at creation, and it was held only in ShareMenu's local state — so reopening the menu on a session that was already sharing showed an empty Link tab, with no route back to the link short of stopping and restarting the share. The first Generate still requires an explicit click, because it creates the multiplayer session (publishes the terminal, consumes a concurrent-session slot); only an already sharing session now shows its link automatically.

Joining by a bare sessionId:token requires a real session id. TeamSessions previously accepted any colon-separated pair while OmniSearch was always strict, so host:22 could reach the join call there. The two now agree. One existing test carried the looser contract and moved to a real UUID — called out because it is a test changed to match tightened behaviour, not a test fixed to match a bug.

Verification

  • tsc --noEmit clean; npm run build (tsc + plugin bundles + vite) clean.
  • 3559 tests, 0 failures across 468 files. New coverage: 9 normalizer tests mirroring the server's rules (including that U is rejected), 6 service tests, 6 resolver tests, 8 SpokenCodeRow tests covering mint-on-demand, grouping, countdown, expiry-drop, regenerate, failure, and interval cleanup on unmount.
  • i18n keys added to all four parity-enforced locales (en, fr, ru, zh). The fr/ru/zh strings are unreviewed by a native speaker.

The countdown test was genuinely flaky before landing: vitest's shouldAdvanceTime let real wall time into the expiry arithmetic, so it passed until real UTC crossed the fixture's expiry and then failed at random. It now fakes Date alongside the timers and advances only when told, and survived six consecutive runs.

Not verified — live click-through

No live UI pass was done. Reaching the new row requires a logged-in Pro account and a real shared session, and the running headless container mounts the primary checkout rather than this worktree. So the mint button, the countdown against a real server clock, and an actual code-based join between two accounts are unproven in the app; unit tests structurally cannot cover them. Worth a pass with the two-account harness before this is considered done.

Known gap

A failed redeem in the omni palette only reaches console.error, because the palette closes on activation. The Join by code modal surfaces the error properly. Left as-is rather than restructuring the palette's dismissal in this PR.

kipavy added 3 commits August 17, 2026 09:31
Six call sites resolved the server URL and JWT with the same four lines and the
same two error keys. listActiveSessions keeps its own copy: it degrades to an
empty list instead of throwing.
The server returns invite_token once, at creation, and it was held only in
ShareMenu's local state — so reopening the menu on a session that was already
sharing showed an empty Link tab, with no way back to the link short of
stopping and restarting the share. Retain it on the connection instead.
Hosts can mint a 10-character Crockford code for an invite-link session and read
it down a phone line; the server kills it after ten minutes, so what lands in
chat scrollback or a clipboard manager is a dead credential rather than a
session-lifetime token.

Minted on demand, not alongside the link, so the window starts when the host
needs it. Guests can paste a code anywhere the other invite shapes already work:
detection stays synchronous on shape, and only activating a join costs a request.

Joining by a bare sessionId:token now requires a real session id. TeamSessions
previously accepted any colon-separated pair, unlike OmniSearch, which had always
been strict; one existing test carried the looser contract and moved to a real
UUID.
@kipavy
kipavy merged commit 5274a35 into dev Aug 17, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant